package org.carabiner.infinitest.depgraph;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 
 * @author <a href="mailto:benrady@gmail.com"Ben Rady</a>
 */
public class DependencyGraph
{
  private Map<String, JavaClass> classNameIndex;
  private JavaClassBuilder builder;
  private Set<JavaClass> testsToRun;
  private Set<JavaClass> changedClasses;
  private Set<JavaClass> changedParents;
  private TestVerifier verifier;

  /**
   * 
   * @param classDirList
   *          Pattern used to match tests against class names
   * @throws IOException
   */
  public DependencyGraph(List<File> classDirList, TestVerifier testVerifier) throws IOException
  {
    verifier = testVerifier;
    classNameIndex = new HashMap<String, JavaClass>();
    builder = new JavaClassBuilder(classDirList);
  }

  public DependencyGraph(File buildPath, RegexVerifier testVerifier) throws IOException
  {
    this(Arrays.asList(new File[]{buildPath}), testVerifier);
  }

  /**
   * Clears out all stored classes and dependency information.
   * 
   */
  public synchronized void clear()
  {
    classNameIndex.clear();
  }

  /**
   * Runs through the classpath looking for changed files and returns the set of tests that need to
   * be run.
   * 
   * @return List
   * @throws IOException
   * @throws IOException
   */
  public synchronized Set<JavaClass> findTestsToRun(Set<File> changedFiles) throws IOException
  {
    // Find changed classes
    changedClasses = findClasses(changedFiles);

    // Loop through all changed classes, adding thier parents (and thier
    // parents)
    // to another set of changed classes
    changedParents = new HashSet<JavaClass>();
    for (JavaClass jclass : changedClasses)
    {
      findParents(jclass);
    }

    // combine two sets
    changedClasses.addAll(changedParents);

    // run through total set, and pick out tests to run
    testsToRun = new HashSet<JavaClass>();
    for (JavaClass jclass : changedClasses)
    {
      if (isTest(jclass))
      {
        // if it's a test, add it to the list of tests to run
        testsToRun.add(jclass);
      }
    }

    return testsToRun;
  }

  private void findParents(JavaClass jclass)
  {
    changedParents.add(jclass);
    Set<JavaClass> parents = jclass.getParents();
    for (JavaClass parentClass : parents)
    {
      // If a parent class hasn't been checked before
      // (and it's not already marked to be run),
      // add it to the list of changed classes
      if (!changedClasses.contains(parentClass) && !changedParents.contains(parentClass) && !parentClass.equals(jclass))
      {
        findParents(parentClass);
      }
    }
  }

  JavaClass findJavaClass(String classname) throws IOException
  {
    JavaClass clazz = classNameIndex.get(classname);
    if (clazz == null)
    {
      clazz = builder.createClass(classname);
      if (clazz != null)
      {
        addToIndex(clazz);
      }
    }
    return clazz;
  }

  private void addToIndex(JavaClass parentClass) throws IOException
  {
    classNameIndex.put(parentClass.getName(), parentClass);
    updateParentRefs(parentClass);
  }

  private void updateParentRefs(JavaClass parentClass) throws IOException
  {
    for (String child : parentClass.getImports())
    {
      JavaClass childClass = findJavaClass(child);
      if (childClass != null && !childClass.equals(parentClass))
      {
        childClass.addParent(parentClass);
      }
    }
  }

  Collection<JavaClass> getClassesFromFile(File file) throws IOException
  {
    Collection<JavaClass> classes = builder.buildClasses(file);
    for (JavaClass clazz : classes)
    {
      if (!classNameIndex.containsKey(clazz.getName()))
      {
        addToIndex(clazz);
      }
      else
      {
        classes.remove(clazz);
        classes.add(classNameIndex.get(clazz.getName()));
        updateParentRefs(clazz);
      }
    }
    return classes;
  }

  private Set<JavaClass> findClasses(Set<File> changedFiles) throws IOException
  {
    Set<JavaClass> changedClasses = new HashSet<JavaClass>();
    for (Iterator iter = changedFiles.iterator(); iter.hasNext();)
    {
      File file = (File) iter.next();
      Collection<JavaClass> classesFound = getClassesFromFile(file);
      for (JavaClass foundClass : classesFound)
      {
        changedClasses.add(foundClass);
      }
    }
    return changedClasses;
  }

  boolean isTest(JavaClass javaClass)
  {
    if (javaClass.isAbstract())
      return false;
    try
    {
      Class clazz = Class.forName(javaClass.getName());
      return verifier.isATest(clazz);
    }
    catch (ClassNotFoundException e)
    {
      throw new RuntimeException(e);
    }
  }

  boolean isIndexed(Class<Object> clazz)
  {
    return classNameIndex.containsKey(clazz.getName());
  }
}
